import java.io.*;
import java.net.*;
import java.util.*;
import java.util.jar.*;
import java.text.*;

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

class Common
{
    public static final String[] suffixes = new String[]{"bytes", "KiB", "MiB", "GiB", "TiB"};
    public static final double block = 1024.0;
    public static final DecimalFormat df1 = new DecimalFormat("#,##0");
    public static final DecimalFormat df2 = new DecimalFormat("#,##0.0");
    
    public static String format_byte_quantity (long q)
    {
        double n = q;
        DecimalFormat fmt = df1;
        for (int k=0; k<suffixes.length; k++)
        {
            String suffix = suffixes[k];
            if (k != 0)
            {
                n /= block;
                fmt = df2;
            }
            
            if (n < block) return fmt.format(n) + " " + suffix;
        }
        return df2.format(n) + " " + suffixes[suffixes.length - 1];
    }
}

interface StatusManager
{
    public void addNewStatus (String status);
    public void setCurrentStatus (String status);
}

interface DataReceivedListener
{
    public void totalQuantityKnown (int totalQuantity);
    public void dataReceived (int amountReceived, int totalReceived);
}

class StatusUpdater implements DataReceivedListener
{
    private StatusManager manager;
    
    private String totalReceivedStr = Common.format_byte_quantity(0);
    private String totalQuantityStr = "???";
    
    private String getCurrentStatusMessage ()
    {
        return this.totalReceivedStr + " / " + this.totalQuantityStr;
    }
    private void update ()
    {
        this.manager.setCurrentStatus(getCurrentStatusMessage());
    }
    
    public StatusUpdater (StatusManager manager)
    {
        this.manager = manager;
        
        this.manager.addNewStatus(getCurrentStatusMessage());
    }
    
    public void totalQuantityKnown (int totalQuantity)
    {
        this.totalQuantityStr = Common.format_byte_quantity(totalQuantity);
        this.update();
    }
    public void dataReceived (int amountReceived, int totalReceived)
    {
        this.totalReceivedStr = Common.format_byte_quantity(totalReceived);
        this.update();
    }
}

public class Launcher
{
    public static final String APP_NAME = "jrobutt";
    
    public static final String REMOTE_VERSION_URL = "http://jrobutt.yeahbag.com/vstring";
    public static final String CURRENT_VERSION_ENTRY_NAME = "vstring";
    public static final String APP_URL = "http://jrobutt.yeahbag.com/JRobutt.jar";
    public static final String APP_FNAME = "JRobutt.jar";
    
    public static final File app_dir;
    public static final File app_file;
    
    static
    {
        String os_name = System.getProperty("os.name");
        
        String dname;
        String root_dir;
        
        if (os_name.toLowerCase().contains("windows"))
        {
            root_dir = System.getenv("APPDATA");
            if (root_dir == null) root_dir = System.getProperty("user.home");
            dname = APP_NAME;
        }
        else
        {
            root_dir = System.getProperty("user.home");
            dname = "." + APP_NAME;
        }
        
        app_dir = new File(root_dir, dname);
        if (!app_dir.isDirectory())
        {
            if (!app_dir.mkdir())
            {
                // TODO -- UNWIND error to user: cannot create directory `jrobutt_dir.toString()`
                // maybe make some attemps to figure out why mkdir failed?
                // eg. jrobutt_dir.isFile()
            }
        }
        app_file = new File(app_dir, APP_FNAME);
    }
    
    public static StatusManager currentStatusManager;
    
    public static int[] parse_vstring (String vstring)
    {
        String[] vv = vstring.split("\\.");
        int[] version = new int[vv.length];
        for (int k=0; k<vv.length; k++)
        {
            version[k] = Integer.parseInt(vv[k]);
        }
        return version;
    }
    
    // returns positive if v1 > v2, 0 if v1 == v2, and negative if v1 < v2
    public static int version_compare (int[] v1, int[] v2)
    {
        int min_length = Math.min(v1.length, v2.length);
        for (int k=0; k<min_length; k++)
        {
            int diff = v1[k] - v2[k];
            if (diff != 0) return diff;
        }
        if (v1.length > v2.length)
        {
            return 1;
        }
        else if (v1.length < v2.length)
        {
            return -1;
        }
        else
        {
            return 0;
        }
    }
    
    public static String exception_2_message (Exception ex)
    {
        return ex.getClass().getName() + ": " + ex;
    }
    
    // this method assumes that the data that comes in through istream
    // is a valid unicode string in [some encoding (i don't know which.
    // maybe the system default?)]
    public static String stream_read_all (InputStream istream)
    {
        // not quite sure how this works
        // gotten from here: http://stackoverflow.com/a/5445161
        return new Scanner(istream).useDelimiter("\\A").next();
    }
    
    public static void connect (InputStream istream, OutputStream ostream, DataReceivedListener listener)
        throws IOException
    {
        int total = 0;
        int N = 1024 * 10; // 10 KiB
        byte[] buffer = new byte[N];
        int bufflen;
        while ((bufflen = istream.read(buffer)) >= 0)
        {
            ostream.write(buffer, 0, bufflen);
            total += bufflen;
            if (listener != null) listener.dataReceived(bufflen, total);
        }
    }
    
    public static void wgetb (URL url, OutputStream ostream, DataReceivedListener listener)
    {
        try
        {
            URLConnection urlconn = url.openConnection();
            if (listener != null)
            {
                int length = Integer.parseInt(urlconn.getHeaderField("Content-Length"));
                listener.totalQuantityKnown(length);
            }
            connect(urlconn.getInputStream(), ostream, listener);
        }
        catch (IOException ex)
        {
            System.err.println(exception_2_message(ex));
            System.err.println("  url: " + url);
        }
    }
    
    public static String jar_get_entry (File jar, String entry_name)
    {
        try
        {
            JarFile jf = null;
            InputStream istream = null;
            try
            {
                jf = new JarFile(jar);
                try
                {
                    istream = jf.getInputStream(jf.getEntry(entry_name));
                    return stream_read_all(istream);
                }
                finally
                {
                    if (istream != null) istream.close();
                }
            }
            finally
            {
                if (jf != null) jf.close();
            }
        }
        catch (IOException ex)
        {
            return null;
        }
    }
    
    public static Object[] get_current_version_tuple ()
    {
        String vstring = jar_get_entry(app_file, CURRENT_VERSION_ENTRY_NAME);
        if (vstring == null)
        {
            // no local version seems to exist
            return null;
        }
        else
        {
            return new Object[]{parse_vstring(vstring), vstring};
        }
    }
    
    public static Object[] get_remote_version_tuple ()
    {
        String remote_version_url_str = REMOTE_VERSION_URL;
        URL remote_version_url;
        try
        {
            remote_version_url = new URL(remote_version_url_str);
        }
        catch (MalformedURLException ex)
        {
            // this shouldn't ever happen and I'm too lazy to do anything
            // about it on the rare chance that it does
            remote_version_url = null;
        }
        InputStream istream = null;
        try
        {
            istream = remote_version_url.openStream();
            String vstring = stream_read_all(istream);
            return new Object[]{parse_vstring(vstring), vstring};
        }
        catch (IOException ex)
        {
            return null;
        }
        finally
        {
            // I hate jabber
            if (istream != null)
            {
                try { istream.close(); }
                catch (IOException ex) { ex.printStackTrace(); }
            }
        }
    }
    
    // returns whether or not the download was successful
    public static boolean download_remote_version ()
    {
        String app_url = APP_URL;
        URL url;
        try
        {
            url = new URL(app_url);
        }
        catch (MalformedURLException ex)
        {
            return false;
        }
        
        FileOutputStream fout = null;
        try
        {
            fout = new FileOutputStream(app_file);
            currentStatusManager.addNewStatus("downloading " + app_url + " ...");
            DataReceivedListener listener = new StatusUpdater(currentStatusManager);
            wgetb(url, fout, listener);
            currentStatusManager.addNewStatus("done!");
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
            return false;
        }
        finally
        {
            // I hate jabber
            if (fout != null)
            {
                try { fout.close(); }
                catch (IOException ex) { ex.printStackTrace(); }
            }
        }
        return true;
    }
    
    public static void launch_app (boolean update_status)
    {
        if (update_status)
        {
            currentStatusManager.addNewStatus("launching " + APP_FNAME + " ...");
        }
        ProcessBuilder pb = new ProcessBuilder("java", "-jar", app_file.getAbsolutePath());
        try
        {
            pb.start();
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    public static void launch_app ()
    {
        launch_app(true);
    }
    
    public static boolean should_download_remote ()
    {
        currentStatusManager.addNewStatus("checking current version number...");
        Object[] current_version_tuple = get_current_version_tuple();
        if (current_version_tuple == null)
        {
            currentStatusManager.addNewStatus("no local version seems to exist");
            return true;
        }
        
        int[] current_version = (int[])current_version_tuple[0];
        String current_vstring = (String)current_version_tuple[1];
        currentStatusManager.addNewStatus("current version: " + current_vstring);
        
        currentStatusManager.addNewStatus("checking latest version number...");
        Object[] remote_version_tuple = get_remote_version_tuple();
        int[] remote_version = (int[])remote_version_tuple[0];
        String remote_vstring = (String)remote_version_tuple[1];
        currentStatusManager.addNewStatus("latest version: " + remote_vstring);
        if (version_compare(remote_version, current_version) > 0)
        {
            currentStatusManager.addNewStatus("a new version exists!");
            return true;
        }
        else
        {
            currentStatusManager.addNewStatus("you seem to have the latest version");
        }
        
        return false;
    }
    
    public static StatusManager do_gui_stuff_and_generate_status_manager (JFrame f)
    { 
        final JPanel p = new JPanel();
        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
        p.setBorder(new EmptyBorder(16, 16, 16, 16));
        p.setPreferredSize(new Dimension(640, 200));
        StatusManager statusManager = new StatusManager()
        {
            private JLabel currentLabel = null;
            private Font font = new Font("Verdana", Font.PLAIN, 12);
            
            public void addNewStatus (String status)
            {
                currentLabel = new JLabel(status);
                currentLabel.setFont(font);
                p.add(currentLabel);
                p.revalidate();
                
                try { Thread.sleep(100); }
                catch (InterruptedException ex) {}
            }
            public void setCurrentStatus (String status)
            {
                if (currentLabel != null)
                {
                    currentLabel.setText(status);
                }
                else
                {
                    addNewStatus(status);
                }
            }
        };
        
        f.add(p);
        f.pack();
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension size = f.getSize();
        f.setLocation((screenSize.width - size.width) / 2, (screenSize.height - size.height) / 2);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);
        
        return statusManager;
    }
    
    public static void main (String[] argv)
    {
        boolean check = true;
        boolean download_anyway = true;
        /*
            check for latest version (default): -c  --check
            force update (no check; download):  -f  --force-update
            do not check; do not download:      -n  --no-update
        */
        for (String arg : argv)
        {
            if (argv[0].equals("-c") || argv[0].equals("--check"))
            {
                check = true;
                download_anyway = true; // actually, this value is now meaningless
            }
            else if (argv[0].equals("-f") || argv[0].equals("--force-update"))
            {
                check = false;
                download_anyway = true;
            }
            else if (argv[0].equals("-n") || argv[0].equals("--no-update"))
            {
                check = false;
                download_anyway = false;
            }
        }
        
        // ew ew ew ew hax this is terrible
        if (!check && !download_anyway)
        {
            launch_app(false);
            return;
        }
        
        JFrame f = new JFrame("JRobutt Launcher");
        currentStatusManager = do_gui_stuff_and_generate_status_manager(f);
        
        boolean download_remote = check ? should_download_remote() : download_anyway;
        if (download_remote)
        {
            download_remote_version();
        }
        launch_app();
        
        f.dispose();
    }
}
